/*
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 * 
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Library General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor Boston, MA 02110-1301,  USA
 */

/**
 * @version base.c 0.0.1
 * @package Comoro
 * @author Dr.NP <np@bsgroup.org>
 *
 * Base server
 */

#include "bspd.h"

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <arpa/inet.h>
#include <time.h>
#include <event.h>
#include <pthread.h>

#ifdef HAVE_MALLOC_H
#ifndef __OpenBSD__
#include <malloc.h>
#endif
#endif

// User session struct
struct session_data
{
	int session_id;
	unsigned long set_time;
	unsigned long long user_id;
	char username[MAX_USERNAME_LENGTH];
	char session_key[SESSION_KEY_LENGTH];
	int app_id;
};

extern struct conf setting;
extern struct event_base *ev_base;
static unsigned long inited_time;
static unsigned long total_package_read, total_package_send, total_command;
static pthread_mutex_t base_lock;
struct session_data *session_list;
static int session_list_size;

/* Base server initialize */
int base_init()
{
	struct conn_server *c = (struct conn_server *) malloc(sizeof(struct conn_server));

	memset(c, 0, sizeof(struct conn_server));
	
	c->srv_addr = inet_addr(setting.base_srv_listen_addr);
	c->srv_port = setting.base_srv_port;
	c->listen_queue_length = 4096;
	c->nonblock = 1;
	c->tcp_nodelay = 1;
	c->tcp_reuse = 1;
	c->tcp_send_buffer_size = 131072;
	c->tcp_recv_buffer_size = 131072;
	c->tcp_timeout = 3;
	c->on_data_callback = base_on_data;
	c->on_conn_callback = base_on_conn;
	c->on_close_callback = base_on_close;

	// A new TCP server
	conn_tcp_server_new(c);

	inited_time = (unsigned long) time((time_t *) NULL);
	total_package_read = total_package_send = total_command = 0;
	session_list = (struct session_data *) malloc(SESSION_LIST_SIZE * sizeof(struct session_data));

	if (!session_list)
	{
		fprintf(stderr, "Session cache list initialized error\n");
		log_add("Session cache list initialized error");

		exit(EXIT_MEMORY_ERROR);
	}
	
	memset(session_list, 0, SESSION_LIST_SIZE * sizeof(struct session_data));
	session_list_size = SESSION_LIST_SIZE;
	log_add("Session cache list initialized as %d", session_list_size);

	// Delete session overdue (default 10 secs)
	queue_clock_cb_func_add(session_list_check_cb, NULL);
	
	return RTN_SUCCESS;
}

/* Callback when data arrival */
void base_on_data(int client_id, void *data, unsigned int len)
{
	unsigned int data_remain = len;
	unsigned int plen;
	unsigned int spa;
	static char status[STATUS_PACKAGE_LENGTH - 4];
	int sid;
	static char signal[8];

	//package_output(data, len);
	// Split data into commands
	while(data_remain > 0)
	{
		if (data_remain >= 4)
		{
			// Read package length
			plen = get_u_int(data + len - data_remain);
			if (plen > SGN_RANGE)
			{
				if (SGN_HEARTBEAT == plen && data_remain >= 8)
				{
					// Heartbeat
					conn_client_hb(conn_client_list_get(client_id));

					data_remain -= 8;
				}

				else if (SGN_SEQ_RESPONSE == plen && data_remain >= 8)
				{
					// Package response
					spa = get_u_int(data + len - data_remain + 4);
					conn_client_seq_del(conn_client_list_get(client_id), spa);
					
					data_remain -= 8;
				}

				else if (SGN_SEQ_REQUEST == plen && data_remain >= 8)
				{
					// Package request
					spa = get_u_int(data + len - data_remain + 4);
					conn_client_seq_send(conn_client_list_get(client_id), spa);

					data_remain -= 8;
				}

				else if (SGN_CMD_STATUS == plen && data_remain >= 8)
				{
					// Fetch status
					pthread_mutex_lock(&base_lock);

					base_get_status(status);
					bridge_to_center(SGN_CMD_STATUS, status, STATUS_PACKAGE_LENGTH - 4);

					pthread_mutex_unlock(&base_lock);

					data_remain -= 8;
					total_command ++;
				}

				else if (SGN_CMD_VALIDATE == plen && data_remain >= 8 + SESSION_KEY_LENGTH)
				{
					// Runner id
					spa = (int) get_u_int(data + len - data_remain + 4);
					sid = session_list_get(data + len - data_remain + 8, spa);

					// Check session
					
					put_u_int(SGN_CMD_VALIDATE, signal);
					spa = get_u_int(data + len - data_remain + 4);
					
					if (sid >= 0)
					{
						// Validate
						fprintf(stderr, "Session validated\n");
						bridge_reg_center(client_id, session_list[sid].user_id, session_list[sid].username, spa);
						session_list_del(sid);
						put_u_int(1, signal + 4);
					}

					else
					{
						// Wrong session
						fprintf(stderr, "Session error\n");
						put_u_int(0, signal + 4);
					}

					conn_client_send(conn_client_list_get(client_id), signal, 8);
					
					data_remain -= 8 + SESSION_KEY_LENGTH;
					total_command ++;
				}

				else
				{
					// Command error
					data_remain = 0;
					
					break;
				}
			}

			else
			{
				if (4 <= plen && data_remain >= plen)
				{
					// Valid package, write to bridge
					base_process_command(client_id, data + len - data_remain + 4, plen - 4);
					data_remain -= plen;

					continue;
				}

				else
				{
					// Half package
					data_remain = 0;

					break;
				}
			}
		}
	}

	return;
}

/* Call back when client connect */
void base_on_conn(int client_id)
{
	//bridge_reg_center(client_id);
	
	return;
}

/* Call back when client offline */
void base_on_close(int client_id)
{
	bridge_unreg_center(client_id);

	return;
}

/* User session arrival */
void base_on_session(unsigned long long user_id, char *username, char *session_key, int app_id)
{
	fprintf(stderr, "Session goes here : %s\n", username);
	session_list_add(user_id, username, session_key, app_id);
	
	return;
}

/* Call back when data arrival from center */
void base_from_center(int client_id, void *data, int len)
{
	// Special command
	if ((unsigned) client_id > SGN_RANGE)
	{
		base_special_command(-1, (unsigned int) client_id, 0);
	}

	else
	{
		total_package_send ++;
		unsigned int seq_id = conn_client_snd_seq_get(conn_client_list_get(client_id)) + 1;
		conn_client_snd_seq_set(conn_client_list_get(client_id), seq_id);
		put_u_int(seq_id, data + 4);
		conn_client_send(conn_client_list_get(client_id), (const char *) data, len);

		// Add a sequence buffer
		conn_client_seq_add(conn_client_list_get(client_id), (char *) data, len);
	}

	return;
}

/* Process command */
int base_process_command(int client_id, char *data, int len)
{
	unsigned int seq_id = get_u_int(data);
	static char signal[8];

	pthread_mutex_lock(&base_lock);
	
	// Sequence missed?
	if (seq_id > conn_client_rcv_seq_get(conn_client_list_get(client_id)) + 1)
	{
		// Request nearest missed package
		put_u_int(SGN_SEQ_REQUEST, signal);
		put_u_int(conn_client_rcv_seq_get(conn_client_list_get(client_id)) + 1, signal + 4);
		conn_client_send(conn_client_list_get(client_id), signal, 8);
	}

	else
	{
		conn_client_rcv_seq_set(conn_client_list_get(client_id), seq_id);
		bridge_to_center(client_id, data + 4, len - 4);

		// Speak to client that i recieved your gift
		put_u_int(SGN_SEQ_RESPONSE, signal);
		put_u_int(seq_id, signal + 4);
		conn_client_send(conn_client_list_get(client_id), signal, 8);
	
		total_package_read ++;
	}

	pthread_mutex_unlock(&base_lock);
	
	return RTN_SUCCESS;
}

/* Special command (Command ID > 0xFFFFFF00) */
int base_special_command(int client_id, unsigned int cmd_id, unsigned int cmd_val)
{
	total_command ++;
	
	if (SGN_HEARTBEAT == cmd_id && 0x0 == cmd_val)
	{
		// Heartbeat
		conn_client_hb(conn_client_list_get(client_id));
	}

	if (SGN_CMD_STATUS == cmd_id)
	{
		static char status[STATUS_PACKAGE_LENGTH - 4];
		pthread_mutex_lock(&base_lock);

		base_get_status(status);
		bridge_to_center(SGN_CMD_STATUS, status, STATUS_PACKAGE_LENGTH - 4);
		pthread_mutex_unlock(&base_lock);
	}
	
	return RTN_SUCCESS;
}

/* Register server to login */
void base_reg_login()
{
	static char signal[12];

	pthread_mutex_lock(&base_lock);
	
	put_u_int(SGN_BASE_REG, signal);
	put_u_int((unsigned int) inet_addr(setting.base_srv_addr), signal + 4);
	put_u_int((unsigned int) setting.base_srv_port, signal + 8);
	
	bridge_to_login(signal, 12);

	queue_clock_cb_func_add(base_report_login, NULL);

	pthread_mutex_unlock(&base_lock);

	return;
}

/* Report client count to login */
void base_report_login(void *arg)
{
	static char signal[8];
	static int timer = 0;

	pthread_mutex_lock(&base_lock);
	
	// Report per 10 secs
	timer = (timer > 10) ? 0 : timer;

	if (++ timer != 2)
	{
		pthread_mutex_unlock(&base_lock);
		return;
	}

	put_u_int(SGN_BASE_INFO, signal);
	put_u_int((unsigned int) conn_client_list_count(), signal + 4);
	bridge_to_login(signal, 8);

	pthread_mutex_unlock(&base_lock);

	return;
}

/* Get server status */
void base_get_status(char *buf)
{
	// 124 bytes needed
	// Write zero for base id
	put_u_int(0, buf);

	// Write zero for listen address
	put_u_int(0, buf + 4);

	// Listen port
	put_u_int((unsigned int) setting.base_srv_port, buf + 8);

	// Start time
	put_u_int((unsigned int) inited_time, buf + 12);

	// Current client
	put_u_int((unsigned int) conn_client_list_count(), buf + 16);

	// Max client
	put_u_int((unsigned int) conn_client_list_max(), buf + 20);

	// Total command
	put_u_int(total_command, buf + 24);

	// Total package read
	put_u_int(total_package_read, buf + 28);

	// Total package send
	put_u_int(total_package_send, buf + 32);

	// Base version
	strcpy(buf + 36, BSP_VERSION);
	
	return;
}

/* Add a session data */
int session_list_add(unsigned long long user_id, char *username, char *session_key, int app_id)
{
	int id = session_list_find_free();

	if (id >= 0)
	{
		session_list[id].session_id = id;
		session_list[id].set_time = (unsigned long) time((time_t *) NULL);
		session_list[id].user_id = user_id;
		strncpy(session_list[id].username, username, MAX_USERNAME_LENGTH);
		memcpy(session_list[id].session_key, session_key, SESSION_KEY_LENGTH);
		session_list[id].app_id = app_id;
	}

	return id;
}

/* Delete a session data */
int session_list_del(int session_id)
{
	if (session_id >= 0 && session_id < session_list_size)
	{
		memset(&session_list[session_id], 0, sizeof(struct session_data));
		fprintf(stderr, "Session cache %d deleted\n", session_id);
	}

	return session_id;
}

/* Find a empty slot */
int session_list_find_free()
{
	int i;

	pthread_mutex_lock(&base_lock);
	
	for (i = 0; i < session_list_size; i ++)
	{
		if (0 == session_list[i].set_time)
		{
			pthread_mutex_unlock(&base_lock);
			return i;
		}
	}

	// Enlarge list
	struct session_data *tmp;

	tmp = realloc(session_list, session_list_size * 2 * sizeof(struct session_data));

	if (tmp)
	{
		memset(tmp + session_list_size * sizeof(struct session_data), 0, session_list_size * sizeof(struct session_data));
		session_list = tmp;
		session_list_size *= 2;
		log_add("Session cache list enlarged to %d", session_list_size);
		pthread_mutex_unlock(&base_lock);

		return i + 1;
	}

	pthread_mutex_unlock(&base_lock);

	return RTN_GENERAL;
}

/* Get a session from list */
int session_list_get(char *session_key, int app_id)
{
	int i;

	for (i = 0; i < session_list_size; i ++)
	{
		if (0 == memcmp(session_key, session_list[i].session_key, SESSION_KEY_LENGTH) && app_id == session_list[i].app_id)
		{
			return i;
		}
	}

	return RTN_GENERAL;
}

/* Session timeout check */
void session_list_check_cb(void *arg)
{
	unsigned long now = (unsigned long) time((time_t *) NULL);
	int i;

	for (i = 0; i < session_list_size; i ++)
	{
		if (session_list[i].set_time && now - session_list[i].set_time > SESSION_CHECK_INTERVAL)
		{
			// Session timeout, delete
			session_list_del(i);
		}
	}

	return;
}
